1   // Licensed under the Apache License, Version 2.0 (the "License");
2   // you may not use this file except in compliance with the License.
3   // You may obtain a copy of the License at
4   //
5   // http://www.apache.org/licenses/LICENSE-2.0
6   //
7   // Unless required by applicable law or agreed to in writing, software
8   // distributed under the License is distributed on an "AS IS" BASIS,
9   // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10  // See the License for the specific language governing permissions and
11  // limitations under the License.
12  
13  package org.apache.tapestry5.internal.pageload;
14  
15  import org.apache.tapestry5.internal.TapestryInternalUtils;
16  import org.apache.tapestry5.internal.services.ComponentInstantiatorSource;
17  import org.apache.tapestry5.internal.services.Instantiator;
18  import org.apache.tapestry5.internal.structure.ComponentPageElement;
19  import org.apache.tapestry5.ioc.Location;
20  import org.apache.tapestry5.ioc.Orderable;
21  import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
22  import org.apache.tapestry5.ioc.internal.util.InternalUtils;
23  import org.apache.tapestry5.ioc.internal.util.TapestryException;
24  import org.apache.tapestry5.model.ComponentModel;
25  import org.apache.tapestry5.model.EmbeddedComponentModel;
26  import org.apache.tapestry5.services.ComponentClassResolver;
27  import org.apache.tapestry5.services.pageload.ComponentResourceSelector;
28  
29  import java.util.HashSet;
30  import java.util.Map;
31  import java.util.Set;
32  
33  public class EmbeddedComponentAssemblerImpl implements EmbeddedComponentAssembler
34  {
35      private final ComponentInstantiatorSource instantiatorSource;
36  
37      private final ComponentAssemblerSource assemblerSource;
38  
39      private final ComponentResourceSelector selector;
40  
41      private final ComponentModel componentModel;
42  
43      private final Location location;
44  
45      private final Map<String, Instantiator> mixinIdToInstantiator = CollectionFactory.newCaseInsensitiveMap();
46  
47      private final Map<String, String[]> mixinsIdToOrderConstraints = CollectionFactory.newCaseInsensitiveMap();
48  
49      /**
50       * Maps parameter names (both simple, and qualified with the mixin id) to the corresponding QualifiedParameterName.
51       */
52      private final Map<String, ParameterBinder> parameterNameToBinder = CollectionFactory.newCaseInsensitiveMap();
53  
54      // The id of the mixin to receive informal parameters. If null, the component itself recieves them.
55      // If the component doesn't support them, they are quietly dropped.
56  
57      private final String informalParametersMixinId;
58  
59      private final String componentPsuedoMixinId;
60  
61      private Map<String, Boolean> bound;
62  
63      /**
64       * @param assemblerSource
65       * @param instantiatorSource
66       *         used to access component models
67       * @param componentClassResolver
68       *         used to convert mixin types to component models
69       * @param componentClassName
70       *         class name of embedded component
71       * @param selector
72       *         used to select template and other resources
73       * @param embeddedModel
74       *         embedded model (may be null for components defined in the template)
75       * @param templateMixins
76       *         list of mixins from the t:mixins element (possibly null)
77       * @param location
78       *         location of components element in its container's template
79       * @param strictMixinParameters
80       *         if true (e.g., the 5.4 DTD) then mixin parameters must be fully qualified
81       */
82      public EmbeddedComponentAssemblerImpl(ComponentAssemblerSource assemblerSource,
83                                            ComponentInstantiatorSource instantiatorSource, ComponentClassResolver componentClassResolver,
84                                            String componentClassName, ComponentResourceSelector selector, EmbeddedComponentModel embeddedModel,
85                                            String templateMixins, Location location, boolean strictMixinParameters)
86      {
87          this.assemblerSource = assemblerSource;
88          this.instantiatorSource = instantiatorSource;
89          this.selector = selector;
90          this.location = location;
91  
92          componentModel = getModel(componentClassName);
93  
94          // Add the implementation mixins defined by the component model.
95  
96          for (String className : componentModel.getMixinClassNames())
97          {
98              addMixin(className, componentModel.getOrderForMixin(className));
99          }
100 
101         // If there's an embedded model (i.e., there was an @Component annotation)
102         // then it may define some mixins.
103 
104         if (embeddedModel != null)
105         {
106             for (String className : embeddedModel.getMixinClassNames())
107             {
108                 addMixin(className, embeddedModel.getConstraintsForMixin(className));
109             }
110         }
111 
112         // And the template may include a t:mixins element to define yet more mixins.
113         // Template strings specified as:
114         for (String mixinDef : TapestryInternalUtils.splitAtCommas(templateMixins))
115         {
116             Orderable<String> order = TapestryInternalUtils.mixinTypeAndOrder(mixinDef);
117             String className = componentClassResolver.resolveMixinTypeToClassName(order.getId());
118 
119             addMixin(className, order.getConstraints());
120         }
121 
122         // Finally (new in 5.3, for TAP5-1680), the component itself can sometimes acts as a mixin;
123         // this allows for dealing with parameter name conflicts between the component and the mixin
124         // (especially where informal parameters are involved).
125 
126         componentPsuedoMixinId = InternalUtils.lastTerm(componentClassName);
127 
128         // A bit ugly ... side-effects PLUS a return value ... but that's final variables
129         // for you.
130         informalParametersMixinId = prescanMixins(strictMixinParameters);
131     }
132 
133     private String prescanMixins(boolean strictMixinParameters)
134     {
135         // Mixin id found to support informal parameters
136 
137         String supportsInformals = null;
138 
139         for (Map.Entry<String, Instantiator> entry : mixinIdToInstantiator.entrySet())
140         {
141             String mixinId = entry.getKey();
142             ComponentModel mixinModel = entry.getValue().getModel();
143 
144             updateParameterNameToQualified(mixinId, mixinModel, strictMixinParameters);
145 
146             if (supportsInformals == null && mixinModel.getSupportsInformalParameters())
147                 supportsInformals = mixinId;
148         }
149 
150         // The component comes last and overwrites simple names from the others.
151 
152         updateParameterNameToQualified(null, componentModel, false);
153 
154         return supportsInformals;
155     }
156 
157     private void updateParameterNameToQualified(String mixinId, ComponentModel model, boolean strictMixinParameters)
158     {
159         for (String parameterName : model.getParameterNames())
160         {
161             String defaultBindingPrefix = model.getParameterModel(parameterName).getDefaultBindingPrefix();
162 
163             ParameterBinderImpl binder = new ParameterBinderImpl(mixinId, parameterName, defaultBindingPrefix);
164 
165             if (mixinId == null)
166             {
167                 // This is a formal parameter of the root component so it's always unqualified.
168                 parameterNameToBinder.put(parameterName, binder);
169             } else
170             {
171                 // This is a formal parameter of a mixin, it must be qualified.
172                 parameterNameToBinder.put(mixinId + "." + parameterName, binder);
173 
174                 // When not strict (the older DTDs, before 5.4), then register an unqualified alias.
175                 if (!strictMixinParameters)
176                 {
177                     parameterNameToBinder.put(parameterName, binder);
178                 }
179             }
180         }
181     }
182 
183     private void addMixin(String className, String... order)
184     {
185         Instantiator mixinInstantiator = instantiatorSource.getInstantiator(className);
186 
187         String mixinId = InternalUtils.lastTerm(className);
188 
189         if (mixinIdToInstantiator.containsKey(mixinId))
190             throw new TapestryException(String.format("Mixins applied to a component must be unique. Mixin '%s' has already been applied.", mixinId), location, null);
191 
192         mixinIdToInstantiator.put(mixinId, mixinInstantiator);
193         mixinsIdToOrderConstraints.put(mixinId, order);
194     }
195 
196     private ComponentModel getModel(String className)
197     {
198         return instantiatorSource.getInstantiator(className).getModel();
199     }
200 
201     public ComponentAssembler getComponentAssembler()
202     {
203         return assemblerSource.getAssembler(componentModel.getComponentClassName(), selector);
204     }
205 
206 
207     public ParameterBinder createParameterBinder(String qualifiedParameterName)
208     {
209         int dotx = qualifiedParameterName.indexOf('.');
210 
211         if (dotx < 0)
212         {
213             return createParameterBinderFromSimpleParameterName(qualifiedParameterName);
214         }
215 
216         return createParameterBinderFromQualifiedParameterName(qualifiedParameterName, qualifiedParameterName.substring(0, dotx),
217                 qualifiedParameterName.substring(dotx + 1));
218     }
219 
220     private ParameterBinder createParameterBinderFromSimpleParameterName(String parameterName)
221     {
222 
223         // Look for a *formal* parameter with the simple name on the component itself.
224 
225         ParameterBinder binder = getComponentAssembler().getBinder(parameterName);
226 
227         if (binder != null)
228         {
229             return binder;
230         }
231 
232         // Next see if any mixin has a formal parameter with this simple name.
233 
234         binder = parameterNameToBinder.get(parameterName);
235 
236         if (binder != null)
237         {
238             return binder;
239         }
240 
241 
242         // So, is there an mixin that's claiming all informal parameters?
243 
244         if (informalParametersMixinId != null)
245         {
246             return new ParameterBinderImpl(informalParametersMixinId, parameterName, null);
247         }
248 
249         // Maybe the component claims informal parameters?
250         if (componentModel.getSupportsInformalParameters())
251             return new ParameterBinderImpl(null, parameterName, null);
252 
253         // Otherwise, informal parameter are not supported by the component or any mixin.
254 
255         return null;
256     }
257 
258     private ParameterBinder createParameterBinderFromQualifiedParameterName(String qualifiedParameterName, String mixinId, String parameterName)
259     {
260 
261         if (mixinId.equalsIgnoreCase(componentPsuedoMixinId))
262         {
263             return createParameterBinderForComponent(qualifiedParameterName, parameterName);
264         }
265 
266         if (!mixinIdToInstantiator.containsKey(mixinId))
267         {
268             throw new TapestryException(
269                     String.format("Mixin id for parameter '%s' not found. Attached mixins: %s.", qualifiedParameterName,
270                             InternalUtils.joinSorted(mixinIdToInstantiator.keySet())), location,
271                     null);
272         }
273 
274         ParameterBinder binder = parameterNameToBinder.get(qualifiedParameterName);
275 
276         if (binder != null)
277         {
278             return binder;
279         }
280 
281         // Ok, so perhaps this is a qualified name for an informal parameter of the mixin.
282 
283         Instantiator instantiator = mixinIdToInstantiator.get(mixinId);
284 
285         assert instantiator != null;
286 
287         return bindInformalParameter(qualifiedParameterName, mixinId, parameterName, instantiator.getModel());
288     }
289 
290     private ParameterBinder bindInformalParameter(String qualifiedParameterName, String mixinId, String parameterName, ComponentModel model)
291     {
292         if (model.getSupportsInformalParameters())
293         {
294             return new ParameterBinderImpl(mixinId, parameterName, null);
295         }
296 
297         // Pretty sure this was not caught as an error in 5.2.
298 
299         throw new TapestryException(String.format("Binding parameter %s as an informal parameter does not make sense, as %s does not support informal parameters.",
300                 qualifiedParameterName, model.getComponentClassName()), location, null);
301     }
302 
303     private ParameterBinder createParameterBinderForComponent(String qualifiedParameterName, String parameterName)
304     {
305         ParameterBinder binder = getComponentAssembler().getBinder(parameterName);
306 
307         if (binder != null)
308         {
309             return binder;
310         }
311 
312         return bindInformalParameter(qualifiedParameterName, null, parameterName, componentModel);
313     }
314 
315 
316     public boolean isBound(String parameterName)
317     {
318         return InternalUtils.get(bound, parameterName) != null;
319     }
320 
321     public void setBound(String parameterName)
322     {
323         if (bound == null)
324             bound = CollectionFactory.newCaseInsensitiveMap();
325 
326         bound.put(parameterName, true);
327     }
328 
329     public int addMixinsToElement(ComponentPageElement newElement)
330     {
331         for (Map.Entry<String, Instantiator> entry : mixinIdToInstantiator.entrySet())
332         {
333             String mixinId = entry.getKey();
334             Instantiator instantiator = entry.getValue();
335 
336             newElement.addMixin(mixinId, instantiator, mixinsIdToOrderConstraints.get(mixinId));
337         }
338 
339         return mixinIdToInstantiator.size();
340     }
341 
342     public Location getLocation()
343     {
344         return location;
345     }
346 
347     public Set<String> getFormalParameterNames()
348     {
349         return new HashSet<String>(componentModel.getParameterNames());
350     }
351 }